#!/usr/bin/perl

##hogger ver 0.3.0 Dev
##parkercrook@gmail.com

# Copyright (C) 2010-2011 Parker Crook

# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.


use strict;
use Getopt::Long qw(:config no_ignore_case bundling);
use XML::Writer;
use IO::File;
use Switch;
use Socket;
use DBI;
use Nmap::Parser;

my ($DBNAME, $DBHOST, $DBTYPE, $DBUSER, $DBPASS);
my ($nmap_file, $nmap_dir, $config_file, $file, $IP);
my $mode = 'file';
my $nmap_path = '/usr/bin/nmap';
my $range = '1-1024';
my $nmap_args = '-PN -sV -p $range';
my $file;
my $readonly = '';
my @ips=();
my @files=();
my %line_options=();
my %config_info=();
my %results=();
my %QUERY=();
$QUERY{DBTEST}
                = qq{CREATE DATABASE IF NOT EXISTS nwbrain};
$QUERY{TBLTEST1}
                = qq{CREATE TABLE IF NOT EXISTS `nwbrain`.`IP_OS` }
                . qq{(`IP` VARBINARY(256) NULL ,}
                . qq{`OS` VARBINARY(256) NULL ,}
                . qq{`HOSTNAME` VARBINARY(256) NULL ,}
				. qq{`timestamp` TIMESTAMP NULL ,}
                . qq{PRIMARY KEY (`IP`) )};
$QUERY{TBLTEST2}
                = qq{CREATE  TABLE IF NOT EXISTS `nwbrain`.`host_detail` }
                . qq{(`IP_OS_FK` VARBINARY(256) NULL ,}
                . qq{`port` VARBINARY(256) NULL ,}
                . qq{`protocol` VARBINARY(256) NULL ,}
                . qq{`application` VARBINARY(256) NULL ,}
                . qq{PRIMARY KEY (`port`, `IP_OS_FK`),}
                . qq{FOREIGN KEY (`IP_OS_FK`) REFERENCES `nwbrain`.`IP_OS` (`IP`) )};
$QUERY{INSERT1} #Set insert query for host table in nwbrain
		= qq{REPLACE INTO}
		. qq{ IP_OS }
		. qq{(IP, OS, HOSTNAME, timestamp) VALUES (?,?,?,now())};
$QUERY{INSERT2} #Set insert query for port/service table in nwbrain
		= qq{REPLACE INTO}
		. qq{ host_detail }
		. qq{(IP_OS_FK, port, protocol, application) VALUES (?,?,?,?)};

sub write_xml { ##final output logic
	my ($xml,$hashref)=@_;
	my ($output, $writer);
	$output = new IO::File(">$xml") or die "Cannot open $xml: $!\n";
	$writer = new XML::Writer(OUTPUT => $output, NEWLINES => 0, DATA_MODE => 1, DATA_INDENT => 1, UNSAFE => 1);
	$writer->startTag('SNORT_ATTRIBUTES');
	$writer->startTag('ATTRIBUTE_TABLE');
	foreach (sort keys %$hashref) {
		#my $IP = inet_ntoa(pack('N', $_));
		if ($$hashref{$_}{'OS'} =~ /^___/ ) {
			print ("No OS set, host neglected from xml.\n");
		} else {
			$writer->startTag('HOST');
			xml_element('IP',$_,$writer);
			$writer->startTag('OPERATING_SYSTEM');
			$writer->startTag('NAME');
			xml_element('ATTRIBUTE_VALUE',$$hashref{$_}{'OS'},$writer);
			$writer->endTag('NAME');
			xml_element('FRAG_POLICY',$$hashref{$_}{'frag3'},$writer);
			xml_element('STREAM_POLICY',$$hashref{$_}{'stream5'},$writer);
			$writer->endTag('OPERATING_SYSTEM');
			$writer->startTag('SERVICES');
			for my $k (sort keys %{$hashref->{$_}{'application'}}) {
				$writer->startTag('SERVICE');
				$writer->startTag('PORT');
				xml_element('ATTRIBUTE_VALUE',$k,$writer);
				$writer->endTag();
				foreach my $k2 (keys %{$$hashref{$_}{'application'}{$k}}) {
					$writer->startTag('IPPROTO');
					xml_element('ATTRIBUTE_VALUE',$k2,$writer);
					$writer->endTag();
					$writer->startTag('PROTOCOL');
					xml_element('ATTRIBUTE_VALUE',$$hashref{$_}{'application'}{$k}{$k2},$writer);
					$writer->endTag();
					$writer->endTag();
				}
			}
			$writer->endTag('SERVICES');
			$writer->endTag('HOST');
		}
}
$writer->endTag('ATTRIBUTE_TABLE');
$writer->endTag('SNORT_ATTRIBUTES');
$writer->end();
$output->close();					
}

sub db_read { #Read Host data from DB
	my ($opts_ref,$results_ref)=@_;
	my ($dsn, $connect, $query, $query_handle, $ip_column, $OS_column, $port_column, $app_column, $protocol_column);
	#Setup source & connect
	$dsn = "dbi:$$opts_ref{DBTYPE}:$$opts_ref{DBNAME}:$$opts_ref{DBHOST}:$$opts_ref{DBPORT}";
	$connect = DBI->connect($dsn, $$opts_ref{DBUSER}, $$opts_ref{DBPASS});
	#Prepare queries
	$query = "select IP,OS,port,application,protocol from IP_OS, host_detail where IP_OS.IP=host_detail.IP_OS_FK ORDER BY IP";
	$query_handle = $connect->prepare($query);
	#Execute query
	$query_handle->execute();
	#Execute query
	$query_handle->execute();
	#Bind Table Columns to Variables
	$query_handle->bind_columns(\$ip_column, \$OS_column, \$port_column, \$app_column, \$protocol_column);
	#Loop through results
	while($query_handle->fetch()) {
		$$results_ref{$ip_column}{'application'}{$port_column}{$protocol_column} = $app_column;
		switch ($OS_column){ #assign frag3 & stream5 policy based on OS
			case /^[Cisco|IOS]/ {
				$$results_ref{$ip_column}{'frag3'} = "Last";
				$$results_ref{$ip_column}{'stream5'} = "last";
				$$results_ref{$ip_column}{'OS'} = "IOS";}
			case /^HP\sJetDirect/ {
				$$results_ref{$ip_column}{'frag3'} = "BSD-right";
				$$results_ref{$ip_column}{'stream5'} = "bsd"; }
			case /^HP-UX 10/ {
				$$results_ref{$ip_column}{'frag3'} = "BSD";
				$$results_ref{$ip_column}{'stream5'} = "hpux10"; }
			case /^HP-UX 11/ {
				$$results_ref{$ip_column}{'frag3'} = "First";
				$$results_ref{$ip_column}{'stream5'} = "hpux"; }
			case /^IRIX/ {
				$$results_ref{$ip_column}{'frag3'} = "BSD";
				$$results_ref{$ip_column}{'stream5'} = "irix"; }
			case /^Linux/ {
				$$results_ref{$ip_column}{'frag3'} = "Linux";
				$$results_ref{$ip_column}{'stream5'} = "linux"; }  ##needs OS assignment
			case /^Mac/ {
				$$results_ref{$ip_column}{'frag3'} = "First";
				$$results_ref{$ip_column}{'stream5'} = "macos"; }
			case /^SunOS\s5/ {
				$$results_ref{$ip_column}{'frag3'} = "First";
				$$results_ref{$ip_column}{'stream5'} = "first"; }
			case /^Solaris/ {
				$$results_ref{$ip_column}{'frag3'} = "Solaris";
				$$results_ref{$ip_column}{'stream5'} = "solaris"; }
			case /^Microsoft Windows Server 200(3|8)/ {
				$$results_ref{$ip_column}{'frag3'} = "Windows";
				$$results_ref{$ip_column}{'stream5'} = "win2003"; }
			case /^Microsoft Windows 200(3|8)/ {
				$$results_ref{$ip_column}{'frag3'} = "Windows";
				$$results_ref{$ip_column}{'stream5'} = "win2003"; }
			case /^Windows Server 200[3,8]/ {
				$$results_ref{$ip_column}{'frag3'} = "Windows";
				$$results_ref{$ip_column}{'stream5'} = "win2003"; }
			case /^Microsoft Windows/ {
				$$results_ref{$ip_column}{'frag3'} = "Windows";
				$$results_ref{$ip_column}{'stream5'} = "windows"; } ##needs OS assignment
			case /^Windows/ {
				$$results_ref{$ip_column}{'frag3'} = "Windows";
				$$results_ref{$ip_column}{'stream5'} = "windows"; }
			else {
				$$results_ref{$ip_column}{'frag3'} = "BSD";
				$$results_ref{$ip_column}{'stream5'} = "bsd"; }
			}
			print "$ip_column, $OS_column, $port_column, $app_column, $protocol_column \n";
		$$results_ref{$ip_column}{'OS'} = $OS_column unless exists $$results_ref{$ip_column}{'OS'};     #except for IOS set above.
	}
}

sub nmaptodb { #Read nmap and output to DB
	my $host = shift; #pop host off front of array
	my $name;
	my $host_ip = $host->addr();
	my $os = $host->os_sig();
	my $hostname = $host->hostname();
	if (!$os->name()){
		if ($host->mac_vendor() =~ /Cisco/) {
			$name = $host->mac_vendor();
			$name = $name.' Device';
		} else {
			$name = $os->vendor();
		}
	} else {
		$name = $os->name();
	}
	next unless $name =~ /^\w|\d/;
	my @Q1values = (
			$host_ip,
			$name     || undef,
			$hostname || undef
			##add timestamp value here
	);
	our $sth_ins1->execute(@Q1values);
	#Below, eval for open ports, if exists, delete host_detail table for host.
	my $sth3 = our $dbh->prepare_cached("DELETE FROM host_detail WHERE IP_OS_FK='$host_ip'");
	#add timestamp criteria
	$sth3->execute();
	foreach ($host->tcp_open_ports) { #loop for tcp services
		my $svc=$host->tcp_service($_);
		my $tcp_temp=$svc->name;
		my @Q2values = (
				$host_ip,
				$_,
				'TCP',
				$tcp_temp || 'unknown'
		);
#               foreach (@Q2values) {print "$_\n";}
		our $sth_ins2->execute(@Q2values);
		@Q2values=(); #flush array
	}
	foreach ($host->udp_open_ports) { #loop for udp services
		my $svc=$host->udp_service($_);
		my $udp_temp=$svc->name;
		my @Q2values = (
				$host_ip,
				$_,
				'UDP',
				$udp_temp || 'unknown'
		);
		our $sth_ins2->execute(@Q2values);
		my $ry = $sth_ins2->execute(@Q2values) ? "ok" : "OOPS! - " . DBI->errstr;
		#printf( "\t..> %-15s : (%4s) : %-s\n", $host->addr, $host->status, $ry );;
		@Q2values=(); #flush array
	}
}

sub config_file_read { #Parse config file for name, value pairs
	my ($conf_file, $conf_val) = @_;
	my ($line, $name, $value);

	open (CONFIG, "$conf_file") || die "Unable to open $conf_file!";
	while (<CONFIG>) {
		$line=$_;
		chomp($line);
		$line=trim($line);
		if (($line !~ /^#/) && ($line ne "")) {
			($name, $value) = split(/=/, $line);
		$$conf_val{$name} = $value;
		}
	} close(CONFIG);
}

sub help { #Runtime Help
print<<__EOT;
  Usage: $0 -C <conf filename> -m <mode> [-n <nmap filename>
     -D <nmap files directory>  -x <host_attribute filename>]
     [-help]

  Options:
  -m Mode to run:
                in   - Reads nmap files and inserts into Database.
                out  - Reads Database and outputs to xml HAB.
                both - Combination of the above (in,out; which also works)
  -n Where the nmap file containing host information lives.
  -x Where you want to create the host_attribute table.xml (Overwrites existing files)
  -C Where the hogger config file lives.
  -D Directory containing multiple nmap files
  -help/? Print this information.

__EOT
exit(0);
}

sub hogger { #Runtime notice

print<<__EOT;
*****************************************************
*       http://code.google.com/p/hogger
*       Making Snorty & your network friends...
*               Copyright (C) 2010-2011 Parker Crook
*               parkercrook\@gmail.com
*****************************************************

__EOT
}

sub cidr_deflator {
	my ($cidr) = @_;
	my @hosts = "";
	#http://www.perlmonks.org/?node_id=304414
	my $n = NetAddr::IP->new( $cidr );
	for my $ip( @{$n->hostenumref} ) {
		push(@hosts, $ip);
	}
	return @hosts;
}

sub trim { #Remove whitespaces from beginning and end of line
    my ($feed) = @_;
    if ($feed){
        $feed=~s/^\s*//;
        $feed=~s/\s*$//;
        return $feed;
    }
}

sub svc_cleanup { #Remove ? from protocols
	my ($feed) = @_;
	if ($feed) {
		$feed=~s/\?$//;
		return $feed;
	}
}

sub ip_todec { # Convert quad IP to decimal IP
	my $ip_address = shift;
	my @octets = split(/\./, $ip_address);
	my $DEC = ($octets[0]*1<<24)+($octets[1]*1<<16)+($octets[2]*1<<8)+($octets[3]);
	return $DEC;
}

sub xml_element { # Create individual xml elements in form <$element>$value</element>
    my ($element,$value,$xmlobject)=@_;
    $xmlobject->startTag($element);
    $xmlobject->characters($value);
    $xmlobject->endTag();
}

hogger();
if($#ARGV==-1){help();}

#read options from command line - takes precedence over config values
GetOptions (
	"m=s"       => \$line_options{mode},
	"x=s"       => \$line_options{xml},
	"readonly"  => \$line_options{readonly},
	"C=s"       => \$line_options{config_file},
	"D=s"       => \$line_options{nmap_dir},
	"dbname=s"  => \$line_options{DBNAME}, #need error checking routines
	"dbhost=s"  => \$line_options{DBHOST}, #need error checking routines
	"dport=s"   => \$line_options{DBPORT}, #need error checking routines - set default to 3306
	"dbtype=s"  => \$line_options{DBTYPE}, #need error checking routines - set default to mysql
	"dbuser=s"  => \$line_options{DBUSER}, #need error checking routines
	"dbpass=s"  => \$line_options{DBPASS}, #need error checking routines
	"help|?"    => sub { help() });

if ($line_options{config_file} && -f $line_options{config_file}){
        &config_file_read($line_options{config_file}, \%config_info);
} else {
       print "You need to specify a valid config file - CLI has been deprecated for this release!\n\n";
       help();
}

if (!$line_options{DBNAME}) {
        $line_options{DBNAME} = ($config_info{'DBNAME'});
    if (!$line_options{DBNAME}) {
        print("You need to specify a DBNAME!\n\n");
        help();
    }
}
if (!$line_options{DBHOST}) {
        $line_options{DBHOST} = ($config_info{'DBHOST'});
    if (!$line_options{DBHOST}) {
        print("You need to specify a DBHOST!\n\n");
        help();
    }
}
if (!$line_options{DBPORT}) {
        $line_options{DBPORT} = ($config_info{'DBPORT'});
    if (!$line_options{DBPORT}) {
        print("You need to specify a DBPORT!\n\n");
        help();
    }
}
if (!$line_options{DBTYPE}) {
        $line_options{DBTYPE} = ($config_info{'DBTYPE'});
    if (!$line_options{DBTYPE}) {
        print("You need to specify a DBTYPE!\n\n");
        help();
    }
}
if (!$line_options{DBUSER}) {
        $line_options{DBUSER} = ($config_info{'DBUSER'});
    if (!$line_options{DBUSER}) {
        print("You need to specify a DBUSER!\n\n");
        help();
    }
}
if (!$line_options{DBPASS}) {
		$line_options{DBPASS} = ($config_info{'DBPASS'});
	if (!$line_options{DBPASS}) {
		print("You need to specify a DBPASS!\n\n");
		help();
	}
}

if ($line_options{mode} eq "both") { $line_options{mode} = "in,out"; }
my @modes = split(/,/,$line_options{mode});
foreach (@modes) {
	if ($_ eq "in" ) { #input - process nmaps
		if (!$line_options{nmap_dir}) {
			$line_options{nmap_dir} = $config_info{'nmap_dir'};
			if ((!$line_options{nmap_dir}) && (!$line_options{nmap_file})) {
				print "Specify an nmap file, or provide a directory of nmap files!\n\n";
				help();
				}
		}
		print("\nPreparing nmap files...\n");
		my $np = new Nmap::Parser;
		my $dbtest;
		$DBUSER = $line_options{DBUSER};
		$DBPASS = $line_options{DBPASS};
		$DBNAME = $line_options{DBNAME};
		$DBHOST = $line_options{DBHOST};
		my $dsn = "DBI:";
		$dsn .= "mysql"; #change this to $DBTYPE for future compatability of other DB's
		$dsn .= ":host=$DBHOST"; # if $DBTYPE eq 'mysql';
		my $sth = '';
		print("\nValidating Database Structures...\n");
		our $dbh = eval { DBI->connect( $dsn, $DBUSER, $DBPASS ) };
		$dbtest = $dbh->prepare( $QUERY{DBTEST} );
			$dbtest->execute or die "Couldn't validate Database: " . $dbh->errstr;
		$dbtest = $dbh->prepare( $QUERY{TBLTEST1} );
			$dbtest->execute or die "Couldn't validate IP_OS table: " . $dbh->errstr;
		$dbtest = $dbh->prepare( $QUERY{TBLTEST2} );
			$dbtest->execute or die "Couldn't validate host_detail table: " . $dbh->errstr;
		$dsn .= ":$DBNAME";
		our $dbh = eval { DBI->connect( $dsn, $DBUSER, $DBPASS ) };
		print("\nPreparing Insert Statements...\n");
		our $sth_ins1 = eval { $dbh->prepare( $QUERY{INSERT1} ) }
			or die "Couldn't prepare statement: " . our $sth_ins1->errstr;
		our $sth_ins2 = eval { $dbh->prepare( $QUERY{INSERT2} ) }
			or die "Couldn't prepare statement: " . our $sth_ins2->errstr;
		#if ($mode = 'file') {
		$np->callback( \&nmaptodb); #used for cached processing of nmap xml files
		$dsn .= ":$DBNAME";
		my $sth = '';
		our $dbh = eval { DBI->connect( $dsn, $DBUSER, $DBPASS ) };
		our $sth_ins1 = eval { $dbh->prepare( $QUERY{INSERT1} ) }
			or die "Couldn't prepare statement: " . $sth_ins1->errstr;
		our $sth_ins2 = eval { $dbh->prepare( $QUERY{INSERT2} ) }
			or die "Couldn't prepare statement: " . $sth_ins1->errstr;
		if ($line_options{nmap_dir} && -d $line_options{nmap_dir}) {
			@files = <$line_options{nmap_dir}/*>;
			foreach $nmap_file (@files) {
				print "\nProcessing file: $nmap_file...";
				$np->parsefile($nmap_file);
			}
		}
		if ($line_options{nmap_file} && -f $line_options{nmap_file}) {
			$np->parsefile($line_options{nmap_file}); #parse nmap xml file
			print "\nProcessing file: $line_options{nmap_file}...";
		}
		print ("Hosts Added!\n");
		#} elsif ($mode = 'scan') {
		#       if (!$IP) {
		#               print "While using the scan mode, you must specify an IP or IP range to scan with the -I switch!\n";
		#               help();
		#       } else {
		#               if ($IP =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) - (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/) {
		#                       @ips = split(/,/,$IP);
		#               } elsif ($IP =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/\d{1,2}/) {
		#                       @ips = cidr_deflator($IP);
		#               } else { @ips = $IP; }
		#print @ips;
		#               my $dsn = "DBI:$DBTYPE";
		#               $dsn .= ":host=$DBHOST"; # if $DBTYPE eq 'mysql';
		#               $dsn .= ":$DBNAME";
		#               my $sth = '';
		#               our $dbh = eval { DBI->connect( $dsn, $DBUSER, $DBPASS ) };
		#               our $sth_ins1 = eval { $dbh->prepare( $QUERY{INSERT1} ) }
		#                               or die "Couldn't prepare statement: " . $sth_ins1->errstr;
		#               our $sth_ins2 = eval { $dbh->prepare( $QUERY{INSERT2} ) }
		#                               or die "Couldn't prepare statement: " . $sth_ins2->errstr;
		#               $np->parsescan($nmap_path, $nmap_args, @ips);
		#       }
		#}
		our $sth_ins1->finish;
		our $sth_ins2->finish;
		$dbh->disconnect;
	} elsif ($_ eq "out" ) { #output - create HAT
		if (!$line_options{xml})  {
			$line_options{xml} = ($config_info{'xml'});
			if (!$line_options{xml}) {
				print("You need to specify a xml file!\n\n");
				help();
			}
		}
		print ("\nConnecting to DB...\n");
		db_read(\%line_options, \%results);
		print ("\nWriting Host Attribute Table...\n");
		write_xml($line_options{xml}, \%results);
		print ("\nHost Attribute File Exported to: $line_options{xml}\n");
	} else {
		print ("No mode designated\n");
		print ("You must designate a mode to run in!\n\n");
		help();
	}
}
print ("\nGo Snorty, Go!\n");
__END__
